<?php

/**
 * 
 * 
 *  依赖注入模式（Dependency Injection）
 2018 
 PHP 设计模式全集 2018 /  依赖注入模式（Dependency Injection）
 
 2.6.1. 目的
 用松散耦合的方式来更好的实现可测试、可维护和可扩展的代码。
 
 2.6.2. 用法
 DatabaseConfiguration 被注入  DatabaseConnection  并获取所需的  $config 。如果没有依赖注入模式， 配置将直接创建  DatabaseConnection 。这对测试和扩展来说很不好。
 
 2.6.3. 例子
 Doctrine2 ORM 使用依赖注入。 例如，注入到  Connection  对象的配置。 对于测试而言， 可以轻松的创建可扩展的模拟数据并注入到  Connection  对象中。
 Symfony 和 Zend Framework 2 已经有了依赖注入的容器。他们通过配置的数组来创建对象，并在需要的地方注入 (在控制器中)。
 */
/**
 *
 * 单例模式被公认为是 反面模式，为了获得更好的可测试性和可维护性，请使用『依赖注入模式』
 * 数据库连接
 * 日志 (多种不同用途的日志也可能会成为多例模式)
 * 在应用中锁定文件 (系统中只存在一个 ...)
 * 
 * @author jiliangliang
 *        
 */
trait  Singleton
{

    /**
     *
     * @var Singleton
     */
    private static $instance;

    /**
     * 通过懒加载获得实例（在第一次使用的时候创建）
     */
    public static function getInstance(...$args): self
    {
        var_dump($args);
        if (! isset(self::$instance)) {
            self::$instance = new static(...$args);
        }
        return self::$instance;
    }

    /**
     * 不允许从外部调用以防止创建多个实例
     * 要使用单例，必须通过 Singleton::getInstance() 方法获取实例
     */
    // private function __construct()
    // {}
    
    /**
     * 防止实例被克隆（这会创建实例的副本）
     */
    private function __clone()
    {}

    /**
     * 防止反序列化（这将创建它的副本）
     */
    private function __wakeup()
    {}
}

class Di
{
    use Singleton;

    private $container = array();

    public function set($key, $obj, ...$arg): void
    {
        /*
         * 注入的时候不做任何的类型检测与转换
         * 由于编程人员为问题，该注入资源并不一定会被用到
         */
        $this->container[$key] = array(
            "obj" => $obj,
            "params" => $arg
        );
    }

    function delete($key): void
    {
        unset($this->container[$key]);
    }

    function clear(): void
    {
        $this->container = array();
    }

    /**
     *
     * @param
     *            $key
     * @return null
     * @throws \Throwable
     */
    function get($key)
    {
        if (isset($this->container[$key])) {
            $obj = $this->container[$key]['obj'];
            $params = $this->container[$key]['params'];
            if (is_object($obj) || is_callable($obj)) {
                return $obj;
            } else if (is_string($obj) && class_exists($obj)) {
                try {
                    $this->container[$key]['obj'] = new $obj(...$params);
                    return $this->container[$key]['obj'];
                } catch (\Throwable $throwable) {
                    throw $throwable;
                }
            } else {
                return $obj;
            }
        } else {
            return null;
        }
    }
}